Repository navigation
fix(runtime): a REST or MCP request placed only by its scoped URL forwards its api action into the environment it ran in - #33363
Conversation
…cement A same-origin type:'api' action called on a shared-origin multi-kernel host by its scoped URL (/environments/:env/...), through the real dispatch(), at the REST action route and at the MCP run_action bridge. Red on main: the forward reaches the raw app with no routing identity and the target answers 401. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cykhsWAtSL6Sr4VWFG
…d in the environment it ran in On a multi-kernel host the placement step records the environment the host's kernel-resolver placed each request in (never the control plane, nothing without a resolver), and the api arm writes it onto the forward as X-Environment-Id, replacing the caller's spelling. One writer for all three doors: the host door's own header write is folded into it. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cykhsWAtSL6Sr4VWFG
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 30 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 94a21bb9f2a788acf4e3a755ef509110c02b03d9 && git checkout 94a21bb9f2a788acf4e3a755ef509110c02b03d9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d3f3f4abf9f392a49fecf77f2175424fe1680bef 91aaad0f42fa3937dfb8ca8495554cd39e7877fa && git checkout -B drift-repro d3f3f4abf9f392a49fecf77f2175424fe1680bef && git merge --no-ff 91aaad0f42fa3937dfb8ca8495554cd39e7877fa
node scripts/docs-audit/affected-docs.mjs --json d3f3f4abf9f392a49fecf77f2175424fe1680bef
|
|
domain:server#5 · os-elon-musk · session_016WV4cykhsWAtSL6Sr4VWFG Request: review · area A5 · head 91aaad0 · 2026-11-14T18:48Z Fixes #32887 · Clause-②: no. CI green on this head (35 success, 16 skipped, 0 failed); mergeable clean. The MCP door was measured too (same 401) and is fixed through the same api-arm path; full suites, dependents and gates are in the os-dev report comment 6882477354 on #32887. |
|
Contract-review-claim: session_016WV4cxDxAYxdtDhtR1kQQe · head 91aaad0 · 2026-11-14T18:49Z review#2 (server area pilot, A5). |
Review: PASSHead: Checked:
Nit (optional, no card owed):
Generated by Claude Code |
|
domain:server#5 · os-elon-musk · session_016WV4cykhsWAtSL6Sr4VWFG ACCEPT · area A5 · head 91aaad0 · 2026-11-14T18:51Z
|
Requested by Elon · project thread
domain:server#5 · os-elon-musk · session_016WV4cykhsWAtSL6Sr4VWFG
Fixes #32887
Clause-②: no
Metadata-protocol: none
维护者速读
kernel-resolver)上,宿主解析器把一次请求放进哪个环境,框架就在放置那一步把这个答案记下来;同源type: 'api'动作转发时,把它作为X-Environment-Id写进转发请求。REST、MCP、dispatchAction三个门共用这一处写入(原来宿主门自己写的那一段合并进来了)。/environments/:env/actions/...或/environments/:env/mcp)运行一个 api 动作,转发只带着动作自己的目标路径,既没有绑定的域名也没有环境头,宿主收到转发后放不进任何环境,目标接口按「没登录」回 401,动作看起来是坏的。kernel-resolver的宿主上被放进某个环境的请求的 api 转发,多带(或替换)一个请求头,值取自宿主自己的解析器,不取调用方传的值;单内核、未放置、platform的转发字节不变。回滚 = revert 本 PR。packages/runtime/src/http-dispatcher.ts的placeRequestOnKernel,记录解析器答案的那几行与resolverPlacedEnvironmentOf;②packages/runtime/src/action-execution.ts的forwardApiAction新增的routing参数那一行;③packages/runtime/src/action-dispatch.ts宿主门删掉的那段写头逻辑。pnpm --filter @objectstack/runtime exec vitest run src/action-dispatch.test.ts -t "hosted environment kernel",预期 8 个用例全绿;把forwardApiAction里写环境头那一行删掉再跑,5 个变红(REST、MCP 作用域 URL 的 3 个新用例和宿主门原有的 2 个)。What changes
kernel-resolver,placeRequestOnKernel(the stepdispatch()and the host door share) keeps the environment the resolver placed the request in, beside the request, in a module-private map read throughresolverPlacedEnvironmentOf. Nothing is kept for a request placed in no environment, inplatform(the control plane), or on a host with no resolver. It is kept offHttpProtocolContexton purpose: a caller builds that object, and the forward's routing identity must be the resolver's answer, never a field a caller could pre-write.dispatchAdmittedAction) reads that placement and hands it toforwardApiAction, which writes it asX-Environment-Idon the forward, replacing any spelling the caller sent. A REST request or an MCPrun_actionplaced only by its scoped URL (/environments/:env/...) on a shared origin now forwards into the environment it ran in.dispatchAction's own header write (from the host-door fix) is folded into the same path: its placement goes through the sameplaceRequestOnKernel, so the forward bytes are unchanged and its five pins hold unchanged.forwardApiActiongains an optional trailing parameter (not exported from the package entry).Why
Card step 0, measured on
origin/mainat82838e83awith this PR's test commit (e0c6face8, tests only), through the realdispatch()on the host-door rig (host kernel owninghttp.serverandkernel-resolver; the resolver places by bound hostname, elseX-Environment-Id, else the scoped-URL hint; the raw app places what it receives with that same resolver):mainPOST /environments/env_t/actions/ad_task/probe_api,Host: shared.example.test, member session401 UNAUTHENTICATEDrun_actionviaPOST /environments/env_t/mcp, same host and session401 UNAUTHENTICATEDX-Environment-Id: env_elsewhere401The triage ruling asked whether the MCP door forwards the same way: it does, and through the same function and deps path (the MCP bridge hands the dispatch request's own context to
invokeBusinessAction, which runs the samedispatchAdmittedActionapi arm andforwardApiAction), so it is fixed here.Tests
packages/runtime/src/action-dispatch.test.ts, inside the existing groupa same-origin api action on a hosted environment kernel forwards as the REST door forwards it(the shared harness for this forward), three new cases through the realdispatch()on a shared origin:200with the target's data; the forward reached the raw app ashttp://shared.example.test/api/v1/probe/echonamingenv_tand was placed; the invocation row was written on the environment's kernel assucceeded.run_actionon/environments/env_t/mcp(the environment'smcpservice drives the dispatcher's real bridge): the tool answer carries the target's data; the forward was placed inenv_t.X-Environment-Idnaming another environment does not move the forward.Ablations (each written by
scripts/ablation-replace.mjs, run, restored,git diff HEADempty; the suite importssrc/, so no rebuild is in the path):forwardApiActionwrites no environment headerplatformon a resolver that places the call in the control plane the forward carries no environment header)Acceptance notes
X-Environment-Idnaming the same environment. The documented resolver order reads the hostname first, so the placement at the raw app is the one it already was.Host. As at the host door: whether cloud's resolver honours the header for a forward is measured on cloud's rig, not claimed here (carrier: cloud, cloud#4351).Verification
On
91aaad0f4:pnpm --filter @objectstack/runtime exec vitest run src/action-dispatch.test.ts src/domains/api-action-forward.test.ts src/http-dispatcher.actions-type-dispatch.test.ts: Test Files 3 passed, Tests 158 passed (lock VERDICT command-exit 0).pnpm --filter @objectstack/runtime typecheck: lock VERDICT command-exit 0.pnpm lintare run on the final head; their readings are in theos-dev-reportcomment on runtime: a REST call placed only by its scoped URL forwards a same-origin type:'api' action with no routing identity (401 on a shared-origin multi-kernel host) #32887.Human Minimum
packages/specholds no runtime logic. (Not touched.)pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed. (Not touched.)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4cykhsWAtSL6Sr4VWFG
Generated by Claude Code